'''
Created on 2018年4月15日

@author: heguofeng
'''
import unittest
import logging
import requests
import json
from pp_network import PPNetApp, Texter
from pp_link import PPMessage, PP_APPID, PPApp, BroadCastId, NAT_TYPE, set_debug,\
    Receiver, NAT_STRING, is_turnable
import struct
import socket
import sys
# from  util import *
import time
from _thread import start_new_thread
import threading
import random
import yaml

def prepare_socket(timeout=10,ip="0.0.0.0",port=0):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    if not sys.platform.startswith("win"):    
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    if timeout:
        sock.settimeout(timeout)
    if port:            
        sock.bind((ip,port))
    return sock

class PPNode(object):
    '''
    .节点信息
    
    '''
    def __init__(self, **kwargs):
        self.node_id = 0
        self.email = ""
        self.phone = ""  
        
        self.ip = ""
        self.port = 0
        self.local_addr = ("0.0.0.0",0)
        self.nat_type = NAT_TYPE['Unknown']
        self.sock = None

 
        
        self.will_to_turn = True  
        self.secret = ""
        self.last_in = int(time.time())
        self.last_out = int(time.time()) - 200
        self.status = False  # unknown,True = alive
        self.turn_server = 0  # 针对非turn节点，其转接点        return self   
        self.distance = 10    
        self.last_beat_addr = (0,0)
        
        # 心跳间隔，若多次没有回复，则可以扩大心跳间隔。在station中设置beat计数器，只有是beat_interval的倍数时，发送beat信息
        self.beat_interval = 1   
        self.byte_in = 0
        self.byte_out = 0
        self.packet_in = 0
        self.packet_out = 0
        self.byte_turn = 0
        self.packet_turn = 0
        self.beat_version = 2

        self.delay = 1000
        
        # 必须得到确认包的 sequence 序列。 
        # 应用中的序列保证 通过应用自身进行 ，建议用sequence 链接
        self.tx_sequence = 0
        self.last_ack = 0
        self.tx_window = 5
        self.tx_buffer = {}
        self.tx_retry = 0
#        self.tx_queue = []   #list of tx_id
        self.tx_queue = {}  # dict of (tx_id:retrycount)
        
        self.rx_sequence = 0

        self.rx_queue = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]  
        
        if "node_dict" in kwargs:
            self.load_dict(kwargs["node_dict"])
        else:
            self.load_dict(kwargs)
        
    def check_turnable(self):
        if self.will_to_turn and is_turnable(self.nat_type):
            return self.status
        return False    
        
    def __str__(self):
        net_id = self.net_id if "net_id" in self.__dir__() else ""
        return "node: %s.%d addr:(%s:%d)  %s(d=%d turn=%d) online=%s %s" % (net_id,self.node_id,
                                                         self.ip, self.port,
                                                         NAT_STRING[self.nat_type], self.distance,self.turn_server,
                                                         self.status,self.sock)
    
    def dump_dict(self, detail=False):
        node_dict = {"node_id":self.node_id, "ip":self.ip, "port":self.port,
                    "nat_type":self.nat_type, "will_to_turn":self.will_to_turn,
                    "secret":self.secret, }
                
        if detail:
            node_dict.update({"status":self.status, "last_out":self.last_out,
                              "last_in":self.last_in, "turn_server":self.turn_server,
                              "beat_interval":self.beat_interval, "distance":self.distance,
                              "tx_queue":self.tx_queue,"delay":self.delay})
        return node_dict
    
    def set_node_status(self,status):
        if status == self.status:
            return 
        logging.info("%s is %s!" % (self.node_id,"offline" if not status else "online"))            
        self.status = status
        if not status:
            self.distance = 10
            if self.sock:
                try:
                    self.sock.close()
                    self.sock = None
                except:
                    pass
    
    def beat_info(self):
        node_info = self.dump_dict(detail=False)
        if not self.status:
            node_info["nat_type"] = NAT_TYPE["Unknown"]
        return node_info

    def load_dict(self, nodedict):
        if "node_id" in nodedict:
            self.node_id = int(nodedict["node_id"])
        if "net_id" in nodedict:
            self.net_id = int(nodedict["net_id"])            
        if "ip" in nodedict:
            self.ip = nodedict["ip"]
        if "port" in nodedict:
            try:
                self.port = int(nodedict["port"])
            except:
                self.port = 0
        if "nat_type" in nodedict:
            self.nat_type = int(nodedict["nat_type"])
        if "will_to_turn" in nodedict:
            self.will_to_turn = nodedict["will_to_turn"]
        if "secret" in nodedict:
            self.secret = nodedict["secret"]
        if "last_in" in nodedict:
            self.last_in = nodedict["last_in"]
        if "last_out" in nodedict:
            self.last_out = nodedict["last_out"]
        if "status" in nodedict:
            self.status = nodedict["status"]
        if "turn_server" in nodedict:
            self.turn_server = nodedict["turn_server"]
        if "beat_interval" in nodedict:
            self.beat_interval = int(nodedict["beat_interval"])     
        if "distance" in nodedict:
            self.distance = int(nodedict["distance"])          
        return self
 
class Listener(PPApp):
    def __init__(self,station):
        self.station = station
        pass
    
    def start(self):
        self.station.sock = prepare_socket(timeout=30,port=self.station.port) 
        ip,port=self.station.ip,self.station.port
        logging.info(" %d waiting connect on %s,%d" % (self.station.node_id,ip,port))             
        self.station.sock.listen(10)
        self.station.sock.settimeout(30)
        start_new_thread(self.listen, ())

    def listen(self):
        while not self.station.quitting:
            try:
                conn, addr = self.station.sock.accept()
            except socket.timeout:
                continue
            except OSError as err:
                logging.debug("%d error in listen %s"%(self.station.node_id,err.__str__()))
                self.station.sock.close()
                self.station.sock = prepare_socket(timeout=30,port=self.station.port)
                self.station.sock.listen(10)
                self.station.sock.settimeout(30)
            else:
                self.accept(conn,addr)
    
    def quit(self):
        try:
            self.station.sock.close()
        except:
            pass
    
    def accept(self,conn,addr):
        self.station.local_addr = conn.getsockname()
        logging.info('Accept connection from : %s local :%s'%(conn.getpeername(),self.station.local_addr))
        new_receive = TCPReceiver(self.station,conn,self.station.process_msg).start()
#         while not self.station.quitting:
#             print(conn.recv(1500))
#         self.station.receive_msg(conn)
#         self.station.beater.start_beat(conn)

class  TCPReceiver(Receiver):
    def __init__(self,station,sock,callback):

        self.sock = sock
        self.remote_addr = sock.getpeername()
        self.local_addr = sock.getsockname()
        self.stage = 1
        self.peer_id = 0
        self.buffer = b""
        logging.info('connection address: %s self address:%s'%(self.remote_addr,self.local_addr))        
        sock.settimeout(90)
        super().__init__(station,callback=callback)
        
    
    def _receive(self):
        while not self.station.quitting and self.stage==1 and self.sock:
            try:
                data = self.sock.recv(1500)
                if not len(data):
                    continue
#                 logging.debug("receive data(%d) %s from %s:%d"%((len(data),data[:20])+self.remote_addr))
                self.station.byte_in += len(data)
                self.station.packet_in += 1            
                self.station.not_runing = 0
                return data
            except socket.timeout:
                return None
            except OSError as exps:
                self.stage = 2
                if self.peer_id:
                    self.station.set_status(self.peer_id,False)
                    logging.warning("%d offline with %s"%(self.peer_id,exps.__str__()))
                try:
                    self.sock.close()
                except:
                    pass
                self.sock = None
                break
        return None
            
    def listen(self):
        while not self.station.quitting and self.stage==1:
            self.receive_msg()
            self.not_runing = 0     

    def receive_msg(self):
        data = self._receive()
        data_len = 0
        if data:
            self.buffer += data
            data_len = len(data)
        buffer_len = len(self.buffer)
        if buffer_len <20:
            return
        start = 0
        while start < buffer_len:
            try:
                ppmsg = PPMessage(bindata=self.buffer[start:])
                logging.debug("%d receive %s (seq=%d)(ttl=%d) from %s to %s addr %s!" % (self.station.node_id,
                                PPMessage.app_string(ppmsg.get("app_id")), ppmsg.get("sequence"),ppmsg.get("ttl"),
                                ppmsg.get("src_id"), ppmsg.get("dst_id"), self.remote_addr))
    #             logging.debug(ppmsg.dict_data)
            except Exception as exp:
                logging.warning("can't decode %s from (%s,%d) Error %s ",data, self.remote_addr, exp)
                break 
            else:
                if ppmsg.get("app_data"):
                    self.process_msg(ppmsg=ppmsg)
                    start += ppmsg.bin_length
#                 else:
#                     if start + ppmsg.bin_length > buffer_len and len(data)==1500:
#                         logging.debug("need receive more buffer len %d need len %d"%(buffer_len-start,ppmsg.bin_length))
#                         #need receive more  
#                         break
#                     else:
#                         start = buffer_len
#                 elif data==None:    #discard
#                     self.buffer = b""
#                     return
                else:
                    if start + ppmsg.bin_length > buffer_len and ppmsg.get("ttl")<7 and ppmsg.bin_length<1500:
                        logging.debug("need receive more buffer len %d need len %d data %d"%(buffer_len-start,ppmsg.bin_length,data_len))
                        #need receive more  
                        break
                    else:
                        #error,discard all buffer
                        logging.debug("something wrong discard %d need len %d"%(buffer_len-start,ppmsg.bin_length))
                        self.buffer=b""
                        return 
        self.buffer = self.buffer[start:]                
            
    def process_msg(self, ppmsg,addr= None,from_id = 0, sock = None):
        if not self.peer_id:
            self.peer_id = ppmsg.get("src_id")
            if self.peer_id and self.peer_id in self.station.peers:
                self.station.peers[self.peer_id].sock = self.sock
                logging.info("%d info set %s "%(self.peer_id,self.station.peers[self.peer_id]))    
            
        ''' call upper layer process'''
        if self.callback:
            self.callback(ppmsg = ppmsg,addr = self.remote_addr,from_id = self.peer_id,sock = self.sock)
        else:
            logging.warning("%s no process seting for %s"%(self.station.node_id,ppmsg.get("app_id")))       
    
class Beater(PPNetApp):
    
    
    class BeatMessageV2(PPApp.AppMessage):
        '''
        beat_cmd :1Byte   beatreq 0x01 beatres 0x02 beatset 0x03 offline 0x04 
        paralen = 3
        parameters:
        net_id = TLV 
        selfnode_info[node TLV]:
            node_id:      4Byte 
            ip:           4Byte
            port:         2Byte
            nattype:      1Byte
            will_to_turn: 1Byte
            secret:       8Byte
        dstnode_info[peer TLV]:
            node_id:      4Byte 
            ip:           4Byte
            port:         2Byte
            nattype:      1Byte
            will_to_turn: 1Byte
            secret:       8Byte
        timestamp: TLV
        
        dict_data={ beat_version:
                    command: 1,
                    parameters:{
                        net_id: 
                        node: {node_id,ip,port,nat_type,will_to_turn,secret}
                        peer: {node_id,ip,port,nat_type,will_to_turn,secret}
                        timestamp:
                        error_info:
                        }
                        
                    }
        '''
        

        def __init__(self, **kwargs):
            tags_id = {
                  "beat_req":1,"beat_res":2,"beat_set":3,"offline":4,"find_node":5,"direct_req":6,"direct_res":7,
                  "bin_node":0x10, "bin_peer":0x11,"bin_path":0x15,
                  "net_id":0x12,"timestamp":0x13,"error_info":0x14}


            parameter_type = {0x10:"s",0x11:"s",0x15:"s",
                              0x12:"str",0x13:"I",0x14:"str"}
            super().__init__(app_id=PP_APPID["Beat"],
                             tags_id=tags_id,
                             parameter_type=parameter_type,
                             **kwargs)
        
        def load(self,bindata):
            super().load(bindata)
            if "bin_node" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["node"] = self._load_node(self.dict_data["parameters"]["bin_node"])
                del self.dict_data["parameters"]["bin_node"]
            if "bin_peer" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["peer"] = self._load_node(self.dict_data["parameters"]["bin_peer"])
                del self.dict_data["parameters"]["bin_peer"]
            if "bin_path" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["path"] = self._load_path(self.dict_data["parameters"]["bin_path"])
                del self.dict_data["parameters"]["bin_path"]
            self.dict_data["parameters"]["beat_version"]=2
            pass
        
        def _load_node(self, bindata):
            node_info = {}
            result = struct.unpack("IIHBB8s", bindata)
            node_info["node_id"] = result[0]
            node_info["ip"] = socket.inet_ntoa(struct.pack('I', socket.htonl(result[1])))
            node_info["port"] = result[2]
            node_info["nat_type"] = result[3]
            node_info["will_to_turn"] = (result[4] == 1)
            node_info["secret"] = result[5].decode()
            return node_info
        
        def dump(self):
            if "node" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["bin_node"] = self._dump_node(self.dict_data["parameters"]["node"])
            if "peer" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["bin_peer"] = self._dump_node(self.dict_data["parameters"]["peer"])
            if "path" in self.dict_data["parameters"]:
                self.dict_data["parameters"]["bin_path"] = self._dump_path(self.dict_data["parameters"]["path"])
            self.dict_data["parameters"]["beat_version"]=2
            return super().dump()
        
        def _dump_node(self, node_info):
            return struct.pack("IIHBB8s",
                               node_info["node_id"],
                               socket.ntohl(struct.unpack("I", socket.inet_aton(node_info["ip"]))[0]),
                               node_info["port"],
                               node_info["nat_type"],
                               1 if node_info["will_to_turn"] else 0,
                               node_info["secret"].encode(),)
            
        def _load_path(self,bin_path,path_len):
            path  = []
            for i in range(0, path_len):
                noderesult = struct.unpack("IIHB", bin_path[i * 11:i * 11+11])
                path.append((noderesult[0],
                               socket.inet_ntoa(struct.pack('I', socket.htonl(noderesult[1]))),
                               noderesult[2],
                               noderesult[3]))
            return path
        
        def _dump_path(self,path):
            data = b""
            for i in range(0, len(path)):
                node = path[i]
                nodedata = struct.pack("IIHB", node[0],
                                       socket.ntohl(struct.unpack("I", socket.inet_aton(node[1]))[0]),
                                       node[2], node[3])
                data += nodedata
            return data      
        
        @staticmethod
        def unpackip(bin_ip):
            return socket.inet_ntoa(struct.pack('I', socket.htonl(struct.unpack("I",bin_ip)[0])))    
        @staticmethod 
        def packip(ip):
            return struct.pack("I",socket.ntohl(struct.unpack("I", socket.inet_aton(ip))[0]))      

    def __init__(self,station,callback=None):
        super().__init__(station,PP_APPID["Beat"],callback)
        self.beat_count = 0
        self.beat_interval = 1        
        self.socks = {}
    
    def start(self):
        super().start()
        start_new_thread(self.beat, ())
        self.beat_null()
        self.station.set_app_process(PP_APPID["Beat"], self.process)
        return self

    def connect_peer(self,peer):
        if peer.turn_server:
            return self.station.peers[peer.turn_server].sock
        if not peer.sock and not peer.ip == "0.0.0.0":
            logging.debug("try to connect %d(%s:%d)"%(peer.node_id,peer.ip,peer.port))
            sock = prepare_socket(timeout=5,port=self.station.port)
            try:
                sock.connect((peer.ip, peer.port))
            except socket.error as exp:
                logging.warning("connect  %d(%s:%d) failure %s"%(peer.node_id,peer.ip,peer.port,exp.__str__()))
#                 print(self.station.node_id,len(self.socks),self.socks)
            else:
                logging.info("connect %d(%s:%d) success with local %s"%(peer.node_id,peer.ip,peer.port,sock.getsockname()))
                peer.sock = sock
                self.socks[(sock.getsockname(),sock.getpeername())] = sock
                self.send_beat(peer.node_id, beat_cmd="beat_req", is_try=True)
                new_receiver = TCPReceiver(self.station,peer.sock,self.station.process_msg).start()
        return peer.sock    
    
    def beat(self):
        try:
            beated = False
            for peerid in self.station.peers:
                peer = self.station.peers[peerid]
                if peer.beat_interval > 3:
                    if peer.status:
                        self.station.set_status(peerid,False)
#                     peer.status = False
#                     peer.distance = 10
                    
                if self.beat_count % peer.beat_interval == 0:
                    peer.beat_interval = peer.beat_interval * 2 if  peer.beat_interval < 65 else 128
                    
                    self.send_beat(peerid, beat_cmd="beat_req")
                    if not self._is_private_ip(peer.ip):
                        beated = True
                
            if not beated:
                self.beat_null()
        except:
            pass
            
        self.beat_count += 1 
        if not self.station.quitting:    
            self.timer = threading.Timer(60 * self.beat_interval, self.beat)
            self.timer.start()
            self.no_beat = 0
            pass     
        
    def send_beat(self, peerid, beat_cmd="beat_req", is_try=False):
        now = time.time()
        if peerid not in self.station.peers:
            logging.warning("can't find %d,please try to find it first." % peerid)
            return False
        
        peer = self.station.peers[peerid]
        if not self.connect_peer(peer):
#             logging.debug("%d: peer %d can't connect!"%(self.station.node_id,peer.node_id))
            peer.set_node_status(False)
#             self.station.path_requester.request_path(peer.node_id)

            return
        
#         logging.debug("%d: peer %d sock %s"%(self.station.node_id,peer.node_id,peer.sock))

#         if peer.ip == "0.0.0.0" : #or peer.ip == self.station.ip:
#             return False 

        beat_dictdata = {"command":beat_cmd,
                         "parameters":{
                             "node":self.station.beat_info(),
                             "peer":peer.beat_info(),
                             "net_id":self.station.net_id,
                             "timestamp":int(time.time())
                             }}
        beat_msg = Beater.BeatMessageV2(dictdata=beat_dictdata)
        
        if beat_cmd == "offline":
            self.send_msg(BroadCastId, beat_msg, always=False)
        else:
            if now - peer.last_out > 10 or beat_cmd == "beat_res" or is_try:
#                 logging.debug("beat_msg %s"%beat_msg.dict_data)
                self.send_msg(peer.node_id, beat_msg, always=True)
                peer.last_out = int(now)
            if not peer.status and not is_try:
                self.station.path_requester.request_path(peer.node_id)

        return True
    
#     def send_beatV2(self,peerid, beat_cmd=_BEAT_CMD["beat_req"], is_try=False):
#         
    
    def process(self,ppmsg,addr,from_id,sock):
        '''
        #修改心跳者信息，若有更新的其他peer信息也同步修改
        #node is beat_src  peer is self
        '''
        btmsg = Beater.BeatMessageV2(bindata=ppmsg.get("app_data"))   
#         logging.debug("%d receive btmsg %s"%(self.station.node_id,btmsg.dict_data))
        beat_version = btmsg.get("parameters")["beat_version"]
        
        net_id = btmsg.get("parameters")["net_id"]
        if not self.station.net_id == "public" and not net_id in (self.station.net_id,"public"):
            logging.warning("%d receive not same net beat,discard %s"%(self.station.node_id,net_id))
            return

        distance = 7 - ppmsg.get("ttl") & 0x0f     
        command = btmsg.get("command")
        parameters = btmsg.get("parameters")
        
        if "error_info" in parameters:
            logging.warning("%d encount beat error %s"%(self.station.node_id,parameters["error_info"]))
            return
            
        if command=="find_node":
            self.path_process(command,parameters)
            return

        if command=="offline":
            self.station.set_status(btmsg.get_parameter("node")["node_id"],False)
            return
                
        if command in ("beat_req","beat_res"):
            if parameters["peer"]["node_id"] == self.station.node_id:
                self.set_self_info(parameters["peer"],addr,distance)            
            self.set_peer_info(command,parameters["node"],parameters["timestamp"],addr,distance,parameters,from_id,sock)
        
        if command == "beat_req":
            self.send_beat(parameters["node"]["node_id"], beat_cmd="beat_res", is_try=True)
            
        if command == "direct_req":
            peer_info = parameters["node"]
            self.direct_connect(self_addr=("0.0.0.0",self.station.port),
                                         peer_addr=(peer_info["ip"],peer_info["port"]))
                        
        return
         
    def beat_null(self):
        '''
        just keep nat firewall know it is runing
        '''
        logging.debug("%d: beat null"%self.station.node_id)

        try:
            sock = prepare_socket(timeout=10,port=self.station.port)
#             sock.bind(("0.0.0.0", self.station.port))
#         sock = self.station.sock
            sock.connect((socket.inet_ntoa(struct.pack('I', socket.htonl(random.randint(1677721600, 1694498816)))),
                                random.randint(10000, 60000)))
        except Exception as exp:
            logging.warning("error in beat null %s: %s "%(sock,exp.__str__()))
            pass
        
    def send_offline(self,node_id=0):
        '''
        node_id 0 self, else othernode
        '''
        if not node_id:
            node_info = self.station.beat_info()
        elif node_id in self.station.peers:
            node_info = self.station.peers[node_id].beat_info()
        else:
            logging.warning("can't find node_id %d"%node_id)
            return
            
        beat_dictdata = {
                    "command":"offline",
                    "parameters":{
                                  "net_id":self.station.net_id,
                                  "node":node_info,
                                  "timestamp":int(time.time()),
                                  }}
#         print(beat_dictdata)
        beat_msg = Beater.BeatMessageV2(dictdata=beat_dictdata)
        self.send_msg(BroadCastId, beat_msg, always=False)   
        
    def _is_private_ip(self, ip):
        if ip.startswith("172.") or ip.startswith("192.") or ip.startswith("10.") or ip.startswith("127."):
            return True
        else:
            return False            
        
    def set_self_info(self,peer,addr,distance=1):
        if not peer["node_id"] == self.station.node_id \
            or peer["ip"] == "0.0.0.0" or self._is_private_ip(peer["ip"]):
            return
        if distance >1:
            if self.station.nat_type==NAT_TYPE["Unknown"] and distance < self.station.distance:
                self.station.nat_type = peer["nat_type"]
                self.station.distance = distance
                logging.debug("set self nattype to %s from undirect peer info" % NAT_STRING[self.station.nat_type])
            return
        if not self.station.status:
            self.station.ip = peer["ip"]
            self.station.port = peer["port"]
            nat_type = peer["nat_type"]
            if not nat_type == NAT_TYPE["Unknown"]:
                self.station.nat_type = nat_type
                self.station.distance = 1 
            logging.info("set self nattype to %s from peer!" % NAT_STRING[self.station.nat_type])
            self.station.status = True
#             self.station.publish()
            self.station.last_beat_addr = addr
            logging.info("%s connect to the world." % self.station.node_id)
        else:
            if self.station.ip == peer["ip"] and self.station.port == peer["port"]:
                if not self.station.last_beat_addr == addr:
                    if not self.station.nat_type == NAT_TYPE["Turnable"]:
                        logging.debug("set self nattype to %s for differen direct connect" % NAT_STRING[self.station.nat_type])
                    self.station.nat_type = NAT_TYPE["Turnable"] 
                    self.station.distance = 1 
                    self.station.last_beat_addr = addr
            else:
                if self.station.ip == "0.0.0.0":
                    self.station.ip = peer["ip"]
                    self.station.port = peer["port"]
                    self.station.nat_type = peer["nat_type"]
                    logging.info("set self info to %s from peer!" % NAT_STRING[self.station.nat_type])
                else:
                    if not self.station.nat_type == NAT_TYPE["Unturnable"]:
                        logging.debug("set self nattype to %s for not same ip port, self (%s:%d) peer(%s:%d)" %(
                             NAT_STRING[self.station.nat_type],self.station.ip,self.station.port,peer["ip"],peer["port"]))
                    self.station.nat_type = NAT_TYPE["Unturnable"]
                    self.station.distance = 1 
                    

    def set_peer_info(self,command,node_info,timestamp,addr,distance,parameters=None,from_id=0,sock=None):
        node_id = node_info["node_id"]
        
        if node_id == 0:
            return 
        if node_id not in self.station.peers:
            self.station.peers[node_id] = PPNode(node_id=node_id)
            if distance == 1:
                self.station.peers[node_id].sock = sock
            else:
                self.station.peers[node_id].turn_server = from_id
            
        peer = self.station.peers[node_id]
        peer.delay = (int(time.time() - timestamp) + peer.delay)/2
        if parameters:
            peer.beat_version = parameters["beat_version"]
            peer.net_id = parameters["net_id"]
       
        #wrong id
        if peer.status  and not self._is_private_ip(peer.ip) \
            and node_info["ip"] == "0.0.0.0" and not node_id == BroadCastId-1:   
            parameters1 = {"error_info":"duplication node_id,maybe some one use the id.",
                          "net_id":self.station.net_id}
            
            btmsg = Beater.BeatMessageV2(dictdata={"command":"beat_res",
                                                   "parameters":parameters1 })        
            msg = PPMessage(dictdata={"src_id":self.station.node_id,
                                      "dst_id":node_info["node_id"],
                                      "app_data":btmsg.dump(),
                                      "app_id":PP_APPID["Beat"],
                                      "sequence":0xffffffff})
            if from_id and from_id in self.station.peers and self.station.peers[from_id].sock:
                self.station.peers[from_id].sock.sendall(msg.dump())
            return
         
        if peer.distance < distance and peer.status:
            return
        
        if not peer.status:
            logging.info("%s is online." % node_id)
        
#         peer.load_dict({"status":True, "beat_interval":1})

        # 部分nat 连接会更改端口
        if distance == 1:
            peer.load_dict(node_info)
            peer.load_dict({"ip":addr[0], "port":addr[1], "distance":distance,"turn_server":0,
                            "status":True, "beat_interval":1})
            if node_info["ip"] == "0.0.0.0":
                self.send_beat(node_id, beat_cmd="beat_res", is_try=True)
        else:
#             if not peer.sock and not peer.turn_server:
#                 if command == "beat_req" :
#                     peer.load_dict(node_info)
#                     peer.load_dict({"nat_type":NAT_TYPE["Turnable"]})
#                     logging.info("peer turnable try to direct connect to : %d,%s,%d,%s" % (peer.node_id, peer.ip, peer.port, NAT_STRING[peer.nat_type]))
#                     self.send_beat(node_id, "beat_req", is_try=True)  

            if distance < peer.distance :
                peer.load_dict(node_info)
                peer.load_dict({"turn_server":from_id})
                
                if not peer.status:
                    self.send_beat(node_id,beat_cmd="direct_req",is_try=True)
                    result = self.direct_connect(self_addr=("0.0.0.0",self.station.port),peer_addr=(peer.ip,peer.port))
                    if result[0]:
                        peer.sock = result[0]
                        peer.load_dict({"turn_server":0,"distance":1})
                        if not result[1] == NAT_TYPE["Unknown"]:
                            self.station.nat_type = result[1] 
                        if not result[2] == NAT_TYPE["Unknown"]:
                            peer.nat_type = result[2]
#                         if result[3]:
#                         self.ip = result[3][0]
#                         self.port = result[3][1]                    
                        self.send_beat(node_id, beat_cmd="beat_req", is_try=True)     
                
                peer.load_dict({"distance":distance,"nat_type":NAT_TYPE["Unturnable"],
                                "status":True, "beat_interval":1})

                
#             if  node_info["nat_type"] in ( NAT_TYPE["Turnable"],NAT_TYPE["Unknown"]) \
#                     and node_info["ip"] not in (peer.ip,"0.0.0.0",self.station.ip) \
#                     and command == "beat_res" :
#                 old_node_info = peer.dump_dict()
#                 peer.load_dict(node_info)
#                 peer.load_dict({"nat_type":NAT_TYPE["Turnable"]})
#                 logging.info("peer turnable try to direct connect to : %d,%s,%d,%s" % (peer.node_id, peer.ip, peer.port, NAT_STRING[peer.nat_type]))
#                 self.send_beat(node_id, "beat_req", is_try=True)   
#                 peer.load_dict(old_node_info)
#                 peer.load_dict({"nat_type":NAT_TYPE["Unturnable"]})
#                 
#             if  self.station.nat_type in (NAT_TYPE["Turnable"] ,NAT_TYPE["Unknown"]) \
#                     and node_info["ip"] not in (peer.ip,"0.0.0.0",self.station.ip) \
#                     and command == "beat_res" :
#                 old_node_info = peer.dump_dict()
#                 peer.load_dict(node_info)
#                 logging.info("self turnable try to direct connect to : %d,%s,%d,%s" % (peer.node_id, peer.ip, peer.port, NAT_STRING[peer.nat_type]))
#                 self.send_beat(node_id,"beat_req", is_try=True)   
#                 peer.load_dict(old_node_info)

                
            if peer.nat_type == NAT_TYPE["Unknown"]:
                peer.nat_type = node_info["nat_type"]
                
    def send_direct_req(self, peerid, self_addr,peer_addr):
        if peerid not in self.station.peers:
            logging.warning("can't find %d,please try to find it first." % peerid)
            return False
        
        peer = self.station.peers[peerid]
        if not self.connect_peer(peer):
            return
        
        beat_dictdata = {"command":"direct_req",
                         "parameters":{
                             "node":{"node_id":self.station.node_id, "ip":self_addr[0], "port":self_addr[1],
                                        "nat_type":0, "will_to_turn":False,"secret":"", },
                             "peer":{"node_id":peerid, "ip":peer_addr[0], "port":peer_addr[1],
                                        "nat_type":0, "will_to_turn":False,"secret":"", },
                             "net_id":self.station.net_id,
                             "timestamp":int(time.time())
                             }}
        beat_msg = Beater.BeatMessageV2(dictdata=beat_dictdata)
        self.send_msg(peer.node_id, beat_msg, always=True)

        return True
                    

    def direct_connect(self,self_addr,peer_addr):
        '''
        try connect,return sock,self_type,peer_type,self_external_addr
        '''
        result = [None,None,None]  # accept sock, connect sock , self_addr
        
        def exchange(sock):
            peer_info = sock.getpeername()
            logging.debug("peer %s:%d"%peer_info)
            sock.sendall(Beater.BeatMessageV2.packip(peer_info[0])+struct.pack("III",peer_info[1],0,0))
            data = sock.recv(16)
#             print("receive exchange data(%d):"%len(data),data)
            if len(data)==16:
                self_info = (Beater.BeatMessageV2.unpackip(data[0:4]),struct.unpack("I",data[4:8])[0])
            else:
                self_info = None
            return self_info
                
            
        def accept(port,result):
            logging.info("waiting accept %s", port)
            global connect_socket
            s = prepare_socket(timeout=5, port=port)
            s.listen(10)
            try:
                conn, addr = s.accept()
                result[0] =  conn
                result[2] =  exchange(conn)
            except socket.timeout:
                return None
            except Exception as exp:
                logging.debug("try accept error %s"%exp)            
            else:
                logging.info("Accept %s connected!", port)

                
#                 if not result:
#                     result.append(conn)
                return conn,addr
        
        def connect(local_addr, addr,result):
            logging.info("connect from %s to %s", local_addr, addr)
            global connect_socket
            s = prepare_socket(timeout= 5 , port=local_addr[1],
#                                 ip=local_addr[0],
                                )
            try:
                s.connect(addr)
                result[1] = s
                result[2] =  exchange(s)
            except socket.error:
                return None
            except Exception as exp:
                logging.debug("try connect error %s"%exp)
            else:
                logging.info("connected from %s to %s success!", local_addr, addr)

                
#                 if not result:
#                     result.append(s)
                return s
        
        threads = {
            '0_accept': threading.Thread(target=accept, args=(self_addr[1],result)),
            '2_connect': threading.Thread(target=connect, args=(self_addr, peer_addr,result)),
        }
        for name in sorted(threads.keys()):
            logging.info('start thread %s', name)
            threads[name].start()
            
        while threads:
            if result[0] or result[1]:
                break                    
            for name in list(threads.keys()):  
                if not threads[name].is_alive() :
                    threads.pop(name)
        self_nat_type = NAT_TYPE["Turnable"] if result[0] else NAT_TYPE["Unknown"]
        peer_nat_type = NAT_TYPE["Turnable"] if result[1] else NAT_TYPE["Unknown"]
        if not (result[0] or result[1]):
            self_nat_type = NAT_TYPE["Unturnable"]
            peer_nat_type = NAT_TYPE["Unturnable"]
        sock = result[0] if self_nat_type ==NAT_TYPE["Turnable"] else result[1]  
        logging.debug(" self %s peer %s  %s",NAT_STRING[self_nat_type],NAT_STRING[peer_nat_type],sock)          
        return sock,self_nat_type,peer_nat_type,result[2]
    
class PathRequester(PPNetApp):
    '''
    pr =  PathRequester(station)
    pr.request_path(node_id)
    '''
    
    class PathReqMessage(PPApp.AppMessage):
        '''
        old  pathreq       
            appid        appdata
             0002        [node_id,pathlimit,pathlen,id0,ip0,port0,nattype0,id1...ids ]
                                                 4      1,       1        4   4   2    1
        new:
            appid    appdata
            0002     cmd,tlv(node_id),tlv(pathlimit),tlv(pathlen),tlv(nodes)
                     nodes = [(id0,ip0,port0,nattype0,)id1...ids ]
                                                 
        command = "path_req":1,"path_res":2,"node_set":3,
        parameters =  "node_id":4,"path_limit":5,"path_len":6,"path":7
        path = [(id0,ip0,port0,nattype0),(id1...),...(ids...)]
        '''    
    
        def __init__(self, **kwargs):
            app_id = PP_APPID["PathReq"]
            tags_id= {"path_req":1,"path_res":2,"node_set":3,
                      "node_id":4,"path_limit":5,"path_len":6,"bin_path":7}
            parameter_type={4:"I",5:"B",6:"B",7:"s"},
            super().__init__(app_id,tags_id,None,parameter_type,**kwargs)
        
        def load_path(self,bin_path,path_len):
            path  = []
            for i in range(0, path_len):
                noderesult = struct.unpack("IIHB", bin_path[i * 11:i * 11+11])
                path.append((noderesult[0],
                               socket.inet_ntoa(struct.pack('I', socket.htonl(noderesult[1]))),
                               noderesult[2],
                               noderesult[3]))
            return path
        
        def dump_path(self,path):
            data = b""
            for i in range(0, len(path)):
                node = path[i]
                nodedata = struct.pack("IIHB", node[0],
                                       socket.ntohl(struct.unpack("I", socket.inet_aton(node[1]))[0]),
                                       node[2], node[3])
                data += nodedata
            return data
       
        def load(self, bindata):
#             super().load(bindata)
#             self.dict_data["parameters"]["nodes"] = self.load_path(self.dict_data["parameters"]["bin_path"],
#                                                                   self.dict_data["parameters"]["path_len"])

            result = struct.unpack("IBB", bindata[:6])
            self.dict_data = {"parameters":{}}
            self.dict_data["parameters"]["node_id"] = result[0]
            self.dict_data["parameters"]["path_limit"] = result[1]
            self.dict_data["parameters"]["path_len"] = result[2]
            self.dict_data["parameters"]["nodes"] = self.load_path(bindata[6:],
                                                                  self.dict_data["parameters"]["path_len"])
            
            return self
        
        def dump(self):
            self.dict_data["parameters"]["bin_path"] = self.dump_path(self.dict_data["parameters"]["nodes"])
#             return super().dump()                        
            
            binlen = len(self.dict_data["parameters"]["bin_path"])
            data = struct.pack("IBB%ds"%binlen,
                               self.dict_data["parameters"]["node_id"],
                               self.dict_data["parameters"]["path_limit"],
                               self.dict_data["parameters"]["path_len"] ,
                               self.dict_data["parameters"]["bin_path"]
                               )
            return data

    def __init__(self,station):
        super().__init__(station,PP_APPID["PathReq"])
        pass

    def request_path(self, destid, path_limit=3, callback=None):

        if self.station.status:
            prmsg = self.PathReqMessage(dictdata={"command":"path_req",
                                                  "parameters":{
                                                        "node_id":destid, 
                                                        "path_limit":path_limit,
                                                        "path_len":1,
                                                        "nodes":[(self.station.node_id, self.station.ip, self.station.port, self.station.nat_type)]
                                                        }})
    
            self.send_msg(BroadCastId, prmsg)
            return True
            
        return False
    
    def process(self, ppmsg, addr):
        
        prmsg = PathRequester.PathReqMessage(bindata=ppmsg.get("app_data"))
        parameters = prmsg.get("parameters")
        path = parameters["nodes"]
        dst_node_id = parameters["node_id"]
        logging.debug("node %d path %s"%(dst_node_id,path))
        if dst_node_id in (self.station.node_id, BroadCastId):
            for i in range(0, len(path)):
                node_id = path[i][0]
                if node_id == self.station.node_id:
                    return  # discard ,must have been process
            # try source and first turn node ,connect and send pathresponse
            
            end = len(path) if len(path) < 2 else 2
            for i in range(0, end):
                node_id = path[i ][0]
                if node_id not in self.station.peers:
                    self.station.peers[node_id] = PPNode(node_id=node_id)
                node = self.station.peers[node_id]
                if not (node.ip,node.port) == (path[i ][1],path[i ][2]): 
                    node.load_dict({"ip": path[i ][1], "port":path[i ][2],
                                     "nat_type":NAT_TYPE["Turnable"],"turn_server":0})
                    self.station.set_status(node_id,False)
    #               # try send beat direct
                    self.station.beater.send_beat(node_id, "beat_req", is_try=True)
            # try beat src by turn node 
            peer = self.station.peers[path[0][0]]
            if end == 2 and path[1][1] not in ("0.0.0.0") and path[1][3] == NAT_TYPE["Turnable"] \
                and not peer.sock:
                peer.load_dict({"turn_server":path[1][0], "nat_type":NAT_TYPE["Unturnable"]})
                self.station.beater.send_beat(path[0][0], "beat_req", is_try=True)
                
                #try direct again
                peer.load_dict({"turn_server":0, "nat_type":NAT_TYPE["Turnable"]})
                self.station.beater.send_beat(path[0][0], "beat_req", is_try=True)
                if not peer.sock:
                    peer.load_dict({"turn_server":path[1][0], "nat_type":NAT_TYPE["Unturnable"]})

        if (dst_node_id != self.station.node_id) or (dst_node_id == BroadCastId):            
            if len(path) >= parameters["path_limit"] or not self.station.nat_type == NAT_TYPE["Turnable"]:
                return  # exceed path_limit
            
            for i in range(0, len(path)):
                node_id = path[i][0]
                if node_id == self.station.node_id:
                    return  # self have been in path
            last_node_id = path[-1][0]
            path.append((self.station.node_id, self.station.ip, self.station.port, self.station.nat_type))
            
            prmsg.set("parameters",{"node_id":dst_node_id, 
                                    "path_limit":parameters["path_limit"],
                                    "path_len":len(path),
                                    "nodes":path})
            
            for node_id in self.station.peers:
                if self.station.peers[node_id].status and not last_node_id == node_id:
                    self.send_msg(node_id, prmsg)

class Session(object):
    '''
    run for bigdata 
    suppose  in one session 
    s = Session(send_process,receive_process)
    s.send()    
        will save buffer in send_buffer,if receive a reget request(session with data=b"") will send the buffer again
        return totalsize,send_pos
    s.receive()
        if data is b"" then resend the buffer
        if some packet miss will call send_process reget the data (session with data=b"")
        return totalsize,receive_pos
    
    '''
    def __init__(self,send_process,receive_process):
        self.send_process,self.receive_process = send_process,receive_process
        self.send_buffer={}  # {start:(data,end)}
        self.receive_buffer = {}  #{start:(data,end)}
        self.receive_pos = 0
        self.send_size = 0
        self.receive_size = 0
        
        self.last_get =  (0,0,0)  #(start,time,count)
        self.lock = threading.Lock()
        
    def send(self,session,data):
        '''
        return (size, pos)
        '''
        session_id = session["id"]
        start = session["start"]
        end = session["end"]
        
        self.send_process(session,data)

        return (self.send_size,end)
    
    def receive(self,session,data):
        '''
        return (size, pos)
        '''
        session_id = session["id"]
        start = session["start"]
        end = session["end"]
        self.receive_process(session,data)
                    
        return  (self.receive_size,self.receive_pos)
#             self.lock.release()
 
class PPStation(PPNode):
    '''
    public 公布自己的信息，其他节点可以查询到自己状态
    will_to_turn 作为中转节点，如果自身是internet，fullcone，则其他节点可能通过该节点转发
    db_file 保存节点信息
    
    '''
    def __init__(self, config={}):
        if config:
            self.config = config
            super().__init__(node_id=config["node_id"],
                             ip = config.get("node_ip", "0.0.0.0"),
                             port = config.get("node_port", 54320),
                             nat_type = config.get("nat_type",5),
                             will_to_turn=config.get("will_to_turn", True),
                             secret=config.get("node_secret", ""))
            self.neighbor = {}
 
            self.db_file = self.config.get("db_file","nodes.pkl")
            self.net_id = config.get("net_id", "public")
            self.public = self.net_id == "public"
            self.node_type = config.get("node_type","server")          
        else:
            raise("Not correct config!")
        self.load_nodes()
        self.process_list = {}

        self.quitting = False
        self.listener = Listener(self)
        self.path_requester = PathRequester(self)
        self.texter = Texter(self)
        self.beater = Beater(self)
#         self.netmanage = NetManage(self)
        
        self.services={"listener":self.listener,"beater":self.beater,
                       "texter":self.texter,
                              "path_requester":self.path_requester,
                              }
        

        pass    

    def start(self):

        for service in self.services:
            self.services[service].start()
        logging.info("pp tcp Station is runing!")  
        return self        
        
    def quit(self):
        logging.info("pp tcp Station %d is quitting..."%self.node_id)
        if self.status:
            self.beater.send_offline()   
        self.quitting = True               
        for service in self.services:
            self.services[service].quit()

         
    
    def get_peers_online(self):
#         socket.setdefaulttimeout(2)
#         natdetector = NATDetector(self.sockfd,self.ip,self.port)
#         nat_type,self.ip,self.port = natdetector.get_nat_info()
#         self.nat_type = NATDetector.get_nat_type_id(nat_type)
#         socket.setdefaulttimeout(None)
        # get peers from webmaster {node_id:(external_ip,external_port,nat_type)}
        payload = {"net_id":self.net_id, "node_id":self.node_id}
        response = requests.get("http://joygame2.pythonanywhere.com/p2pnet/public", params=payload)
        peers = json.loads(response.text)
 
        # {peer:(external_ip,external_port,nat_type)}
        peernodes = {}
        for peer in peers:
            if not int(peer) == self.node_id and peers[peer][0] and peers[peer][1]:
                peernodes[int(peer)] = PPNode(node_id=peer, ip=peers[peer][0], port=peers[peer][1], nat_type=peers[peer][2]) 
        return peernodes   
    
    def get_status(self, node_id):
        if node_id in self.peers:
            return self.peers[node_id].status
        else:
            logging.warning("Can't found %d in peers." % node_id)
            return False
        
    def set_status(self,node_id,status):
        if node_id in self.peers and not status == self.peers[node_id].status:
            self.peers[node_id].set_node_status(status)
            for nid in self.peers:
                if self.peers[nid].turn_server == node_id:
                    self.set_status(nid, status)
            if not status:
                self.beater.send_offline(node_id)
    
    def set_app_process(self, appid, app_process):
        if appid == PP_APPID["Beat"]:
            self.process_list[appid] = lambda x,y,z,w : app_process(x,y,z,w)
        else:
            self.process_list[appid] = lambda x,y,z,w : app_process(x,y)
        pass
    
    def get_app_process(self, appid):
        if appid in self.process_list:
            return self.process_list[appid]
        else:
            return None
        pass    
   

    def _check_status(self):
        for peerid in self.peers:
            if self.peers[peerid].status:
                return True
                break
        return False

    def send_msg(self, peer_id, app_msg, need_ack=False, always=False):
        '''
        return sequence    0 if failure
        if need_ack must have ack,will retry 3 times if no_ack, then call ack_callback(sequence,False)
        ack_call back can be set by self.ack_callback = func  func(sequence,ackstatus) True have ack False else
        
        '''
        # 如果指定发送对象，则发给指定对象，如果未指定，则发送给节点中的所有 可连接节点
        # 检查是否在peers中，如果不在，
        if peer_id == BroadCastId:  # broadcast
            for peerid in self.peers:
                if self.peers[peerid].status:  # check_turnable():
                    self.send_msg(peerid, app_msg, False, always)
        else:  # unicast
            if peer_id in self.peers:
                peer = self.peers[peer_id]
                app_data = app_msg.dump()
                ppmsg = PPMessage(dictdata={"src_id":self.node_id, "dst_id":peer.node_id,
                                            "app_data":app_data, "app_len":len(app_data),
                                            "app_id":app_msg.get("app_id")})
#                 if peer.status or always:
                return self.send_ppmsg(peer, ppmsg, need_ack)

            print("can't communicate to %s" % peer_id)
            return 0
        
    def send_ppmsg(self,peer, ppmsg, need_ack=False):
        
        if ppmsg.get("sequence") == 0:
            self.tx_sequence += 1
            sequence = self.tx_sequence
            ppmsg.set("sequence", sequence)

        if peer.turn_server:
            if self.peers[peer.turn_server].sock:
                try:
                    self.peers[peer.turn_server].sock.sendall(ppmsg.dump())
                except:
                    self.peers[peer.turn_server].sock =  None
        elif peer.sock:
            try:
                if peer.sock.fileno()>0:
                    peer.sock.sendall(ppmsg.dump())
                else:
                    peer.sock = None
            except socket.timeout:
                pass
            except:
                peer.sock = None
        else:
            logging.warning("not connect,can't send!")
        
    
    def process_msg(self, ppmsg, addr=None,from_id=0,sock=None):
        
        dst_id = ppmsg.get("dst_id")
        if dst_id == self.node_id or dst_id == BroadCastId:
            sequence = ppmsg.get("sequence")
            src_id = ppmsg.get("src_id")
            app_id = ppmsg.get("app_id")
            if src_id in self.peers:
                peer = self.peers[src_id]
                
#                 if sock:
#                     peer.sock = sock
                peer.last_in = int(time.time())
                peer.byte_in += len(ppmsg.get("app_data")) + 20
                peer.packet_in += 1

                if not app_id == PP_APPID["Beat"] :
                    # peer.status = True
                    peer.beat_interval = 1
            app_id = ppmsg.get("app_id")
            process = self.process_list.get(app_id, None)
            
            if process :  # and sequence > self.peers[src_id].rx_sequence:
                process(ppmsg,addr,from_id,sock)
            else:
                logging.warning("%s no process seting for %s"%(self.node_id,ppmsg.get("app_id")))

        else:
            if dst_id in self.peers:
                logging.debug("%d forward to %s " % (self.node_id, str(dst_id)))
                ttl = ppmsg.get("ttl") - 1 
                if ttl & 0x0f > 0:
                    ppmsg.set("ttl", ttl)
                    self.send_ppmsg(self.peers[dst_id], ppmsg)
                    self.byte_turn += len(ppmsg.get("app_data")) + 20
                    self.packet_turn += 1
        pass
        
    def set_ipport(self, peer_id, ip="0.0.0.0", port=0):
        if peer_id not in self.peers:
            self.peers[peer_id] = PPNode(node_id = peer_id)
        self.peers[peer_id].ip = ip
        self.peers[peer_id].port = port
        self.peers[peer_id].nat_type = NAT_TYPE["Unturnable"]
        self.set_status(peer_id,False)
        self.beater.send_beat(peer_id, beat_cmd = "beat_req", is_try=True)
                    
    def set_route(self, peer_id, turn_id):
        if peer_id not in self.peers:
            self.peers[peer_id] = PPNode(node_id = peer_id)
        if turn_id in self.peers and peer_id in self.peers:
            self.peers[peer_id].turn_server = turn_id
            self.set_status(peer_id,False)
            self.beater.send_beat(peer_id, beat_cmd = "beat_req", is_try=True)

    def try_direct(self, peer_id):
        if peer_id in self.peers:
            peer = self.peers[peer_id] 
            if peer.distance == 1:
                return True
            self.beater.send_beat(peerid=peer_id, beat_cmd="direct_req", is_try=True)
            result = self.beater.direct_connect(self_addr = (self.ip,self.port), 
                                       peer_addr = (peer.ip,peer.port))
            #sock,self_nat_type,peer_nat_type,self_addr
            if result[0]:
                peer.sock = result[0]
                if not result[1] == NAT_TYPE["Unknown"]:
                    self.nat_type = result[1] 
                if not result[2] == NAT_TYPE["Unknown"]:
                    peer.nat_type = result[2]
                if result[3]:
                    self.ip = result[3][0]
                    self.port = result[3][1]

        
    def get_all_nodes(self, delay=2):
        time.sleep(delay)
#         self.request_path(BroadCastId, 6)
        time.sleep(delay)

        print("Self Info:\n%s\nPeers Info:" % self)
        for peerid in self.peers:
            print(self.peers[peerid])
            
        return self.peers
    
    def dump_nodes(self):
#        pickle.dump( self.peers, open( self.db_file, "wb" ) ) 
        with open(self.db_file, "w") as f:
            f.write(self.json_nodes())
        pass
    
    def json_nodes(self, detail=False):
        nodes_dict = {}
        for peer in self.peers:
            nodes_dict[peer] = self.peers[peer].dump_dict(detail)
#         nodes_dict[self.node_id] = self.dump_dict(detail)
        return json.dumps(nodes_dict)
    
    def load_nodes(self):
        self.peers = {}
        try:
#            self.peers = pickle.load(open(self.db_file, "rb" ))
            with open(self.db_file, "rt") as f:
                nodes = json.loads(f.read())
                for node in nodes:
                    self.peers[int(node)] = PPNode().load_dict(nodes[node])
                    self.peers[int(node)].beat_interval = 1
                if self.node_id in self.peers:
#                     self.load_dict(self.peers[self.node_id].dump_dict())
                    del self.peers[self.node_id]
        except IOError:
            logging.warning("can't load nodes from %s" % self.db_file)
        if not self.peers:
            print("load peer online:")
            self.peers = self.get_peers_online()
        logging.debug(self.db_file)
        logging.debug(self.json_nodes())
        pass
    
    def wait_status(self,node_id,timeout=5):
        count = 0
        while not self.get_status(node_id):
            time.sleep(1)
            count += 1
            if count>timeout:
                break
        return self.get_status(node_id)
    
    def support_commands(self):
        return ["beat","find","p2p","cast","route","ipport","help","quit","stat","set"]
    
    def run_command(self,command_string):
        cmd = command_string.split(" ") 
        #
        if cmd[0]=="beat" and len(cmd)>=2:
            node_id = int(cmd[1])
            self.beater.send_beat(node_id,"beat_req", is_try=True)
            if self.wait_status(node_id, timeout=3):
                self.texter.send_text(node_id,str(node_id)+" is online!",echo=True)
            else:
                print("%d is offline!"%(node_id))
        elif cmd[0]=="find" and len(cmd)>=2:
            node_id = int(cmd[1])
            self.path_requester.request_path(node_id)
            time.sleep(2)
            if node_id in self.peers:
                print(self.peers[node_id])
            else:
                print("can't find path to %d"%node_id)
        elif cmd[0]=="p2p" and len(cmd)>=3:
            self.texter.send_text(int(cmd[1]),command_string[5+len(cmd[1]):])
        elif cmd[0]=="cast":
            self.texter.send_text(0xffffffff,command_string[5:])
        elif cmd[0]=="route" and len(cmd)>=3:
            self.set_route(peer_id=int(cmd[1]), turn_id= int(cmd[2]))
        elif cmd[0]=="direct" and len(cmd)>=2:
            self.try_direct(peer_id=int(cmd[1]))            
        elif cmd[0]=="ipport" and len(cmd)>=4:
            self.set_ipport(peer_id=int(cmd[1]), ip= cmd[2],port = int(cmd[3]))    
        elif cmd[0]=="set" and len(cmd)>=3 and cmd[1]=="nattype":
            self.nat_type = int(cmd[2])         
        elif cmd[0]=="stat":
            if len(cmd) == 2:
                peer_id = int(cmd[1])
                if peer_id == self.node_id:
                    print(self.dump_dict(detail = True))
                else:
                    print(self.peers[peer_id].dump_dict(detail = True))
            else:
                self.get_all_nodes(delay=0)
                print("Services:",self.services.keys())
        elif cmd[0]=="help":
            response = requests.get("http://joygame2.pythonanywhere.com/ppnet/help.txt")
            print(response.text.encode(response.encoding).decode())      
        elif cmd[0]=="quit":
            self.dump_nodes()
            self.quit()
        else:
            return False
        return True

def main(config):
    print("PPNetwork is lanching...")
    station = PPStation(config=config)
    station.start()
    try_count = 0
    while not station.status:
        time.sleep(2)
        station.status = station._check_status()
        try_count += 1
        if try_count > 1 or station.quitting:
            break
    print("node_id=%d online=%s" % (station.node_id, station.status))
    
#     if station.status:
#         station.path_requester.request_path(BroadCastId, 6)
    node_type = config.get("node_type","server")
    is_client = node_type == "client"
    while not is_client and not station.quitting:
        time.sleep(3)
        
    s= "help"
    while not station.quitting:
        try:
            station.run_command(s)
            print("\n%d>"%station.node_id,end="")
#         except Exception as exp:
#             print(exp.__str__())
        finally:
            pass
        if not station.quitting:
            s=input()

    print("PPNetwork Quit!")
    


if __name__ == '__main__':
    config = {"auth_mode":"secret", "secret":"password",
                   "share_folder":".", "net_id":"public",
                   "pp_net":"home", "node_id":100,
                   "node_port":54320, "DebugLevel":logging.WARNING}
    config = yaml.load(open("fmconfig2.yaml"))
    set_debug(config.get("DebugLevel", logging.WARNING),
              config.get("DebugFile", ""))

    main(config=config)
    
    pass

class Test(unittest.TestCase):


    def setUp(self):
        set_debug(logging.DEBUG, "")
        pass


    def tearDown(self):
        pass


    def TtestName(self):
        station = PPStation(config={"node_id":100, "node_ip":"180.153.152.193", "node_port":54330, "nat_type":NAT_TYPE["Turnable"],
                                                             "db_file":"nodesA.pkl","ip":"0.0.0.0"})
        station.start()
        pass
    
    def test_BeatMessage(self):
        node_info = {"node_id":100,
                      "ip":"0.0.0.0",
                      "port":234,
                      "nat_type":5,
                      "will_to_turn":True,
                      "secret":"password"}
        dictdata = {
                    "command":"beat_req",
                    "parameters":{
                                  "net_id":"home",
                                  "node":node_info,
                                  "peer":node_info,
                                  "timestamp":int(time.time()),
                                  }
                    }
        btmsg = Beater.BeatMessageV2(dictdata=dictdata)
        bin = btmsg.dump()
        btmsg2 = Beater.BeatMessageV2(bindata=bin)

        self.assertDictEqual(dictdata["parameters"]["node"], btmsg2.dict_data["parameters"]["node"], "test beatmessage")
        self.assertEqual(dictdata["command"], btmsg2.dict_data["command"], "test beatmessage")
        self.assertEqual(dictdata["parameters"]["net_id"], btmsg2.dict_data["parameters"]["net_id"], "test beatmessage")
        self.assertEqual(dictdata["parameters"]["timestamp"], btmsg2.dict_data["parameters"]["timestamp"], "test beatmessage")   
         
        ppmsg = PPMessage(dictdata={"src_id":100, "dst_id":200,
                                            "app_data":bin, "app_len":len(bin),
                                            "app_id":btmsg.get("app_id")})
        print(ppmsg.dict_data)
        ppmsg2 = PPMessage(bindata = ppmsg.dump())
        print(ppmsg2.dict_data)


if __name__ == "__main__":
    #import sys;sys.argv = ['', 'Test.testName']
    unittest.main()